package com.zhinengshidai.mcp.servers.filesystem;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.zhinengshidai.mcp.servers.common.McpServerManager;
import com.zhinengshidai.mcp.servers.common.annotation.Tool;
import io.modelcontextprotocol.spec.McpSchema;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 * @Description
 * @Author chenss
 * @CreateTime 2025-03-31 10:40:12
 * @ModifyTime
 */
public class FileSystemMCPServer {

    private static final String fileSchema = """
            {
              "type" : "object",
              "properties" : {
                 "path" : {
                     "type" : "string"
                 }
               }
            }
            """;
    private static final String findFileSchema = """
            {
              "type" : "object",
              "properties" : {
                 "path" : {
                     "type" : "string"
                 },
                 "file" : {
                     "type" : "string"
                 }
               }
            }
            """;


    private static final String multiFileSchema = """
            {
              "type" : "object",
              "properties" : {
                 "paths" : {
                     "type" : "array",
                     "items" : {
                        "type" : "string"
                     },
                     "description" : "An array of file paths"
                 }
               }
            }
            """;

    private static final String fileWriteSchema = """
            {
              "type" : "object",
              "properties" : {
                 "path" : {
                     "type" : "string"
                 },
                 "data" : {
                     "type" : "string"
                 },
                 "append" : {
                     "type" : "boolean"
                 },
                 "isBytes" : {
                     "type" : "boolean",
                     "description" : "if bytes data encode by base64, then true ,else false"
                 }
               }
            }
            """;


    private static final String fileModifySchema = """
            {
              "type" : "object",
              "properties" : {
                 "path" : {
                     "type" : "string"
                 },
                 "lines": {
                     "type" : "array",
                     "items" : {
                        "type" : "object",
                        "properties" : {
                           "line_num" : {
                              "type" : "int",
                              "description" : "need modify line's index"
                           },
                           "newContent" : {
                              "type" : "string",
                              "description" : "need modify line's content"
                           },
                           "insert" : {
                              "type" : "boolean",
                              "description" : "if insert a new line then true else false"
                           }
                        }
                     }
                 }
               }
            }
            """;

    private static final String fileDeleteLinesSchema = """
            {
              "type" : "object",
              "properties" : {
                 "path" : {
                     "type" : "string"
                 },
                 "lines": {
                     "type" : "array",
                     "items" : {
                        "type" : "object",
                        "properties" : {
                           "line_num" : {
                              "type" : "int",
                              "description" : "need delete line's index"
                           }
                        }
                     }
                 }
               }
            }
            """;

    private static final String writeMultiFileSchema = """
            {
              "type" : "object",
              "properties" : {
                 "paths" : {
                    "type" : "array",
                    "items" : {
                        "type": "object",
                        "properties":{
                            "path" : {
                                "type": "string"
                             },
                            "data" : {
                               "type": "string"
                            },
                            "append" : {
                                "type" : "boolean"
                            },
                            "isBytes" : {
                                "type" : "boolean",
                                "description" : "if bytes data encode by base64, then true ,else false"
                            }
                         }
                    },
                    "description":"a array contains objects, each object with properties: file path and data "
                }
              }
            }
            """;

    @Tool(description = "read text file", schema = fileSchema)
    public String readTextFile(Map<String, Object> params) throws IOException {
        JSONObject jsonObject = new JSONObject();
        String path = params.get("path").toString();
        jsonObject.put("path", path);
        Path dataPath = Path.of(path);
        if (Files.exists(dataPath)) {
            List<String> fileAllLines = getFileAllLines(dataPath);
            jsonObject.put("result", fileAllLines);
        } else {
            jsonObject.put("result", "file not exist");
            jsonObject.put("isError", true);
        }
        return jsonObject.toString();
    }

    @Tool(description = "read bytes file, return a base64 string", schema = fileSchema)
    public String readBytesFile(Map<String, Object> params) throws IOException {
        JSONObject jsonObject = new JSONObject();
        String path = params.get("path").toString();
        jsonObject.put("path", path);
        Path dataPath = Path.of(path);
        if (Files.exists(dataPath)) {
            byte[] bytes = Files.readAllBytes(dataPath);
            jsonObject.put("result", Base64.getEncoder().encodeToString(bytes));
        } else {
            jsonObject.put("result", "file not exist");
            jsonObject.put("isError", true);
        }
        return jsonObject.toString();
    }


    @Tool(description = "read multi text file", schema = multiFileSchema)
    public List<String> readMultiTextFile(Map<String, Object> params) {
        JSONArray paths = JSONArray.parseArray(JSONArray.toJSONString(params.get("paths")));
        return paths.stream().map(y -> {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("path", y);
            try {
                Path path = Path.of(y.toString());
                if (Files.exists(path)) {
                    ;
                    jsonObject.put("result", getFileAllLines(path));
                } else {
                    jsonObject.put("result", "file not exist");
                    jsonObject.put("isError", true);
                }
            } catch (IOException e) {
                jsonObject.put("result", JSONObject.toJSONString(e));
                jsonObject.put("isError", true);
            }
            return jsonObject.toString();
        }).toList();
    }

    private List<String> getFileAllLines(Path path) throws IOException {
        List<String> allLines = Files.readAllLines(path, StandardCharsets.UTF_8);
        List<String> objects = new ArrayList<>();
        IntStream.range(0, allLines.size()).forEach(x -> {
            String lineContent = allLines.get(x);
            FileLine fileLine = new FileLine(x + 1, lineContent);
            objects.add(fileLine.toString());
        });
        return objects;
    }

    @Tool(description = "delete file", schema = fileSchema)
    public String deleteFile(Map<String, Object> params) {
        JSONObject jsonObject = new JSONObject();
        String path = params.get("path").toString();
        jsonObject.put("path", path);
        jsonObject.put("result", new File(path).delete());
        return jsonObject.toString();
    }

    @Tool(description = "delete multi file", schema = multiFileSchema)
    public List<String> deleteMultiFile(Map<String, Object> params) {
        JSONArray paths = JSONArray.parseArray(JSONArray.toJSONString(params.get("paths")));
        return paths.stream().map(x -> {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("path", x);
            jsonObject.put("result", new File(x.toString()).delete());
            return jsonObject.toString();
        }).toList();
    }

    @Tool(description = "create empty file", schema = fileSchema)
    public String createEmptyFile(Map<String, Object> params) throws IOException {
        String path = params.get("path").toString();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("path", path);
        File file = new File(path);
        if (file.exists()) {
            jsonObject.put("result", "file already exist");
        } else {
            File dir = file.getAbsoluteFile().getParentFile();
            if (!dir.exists()) {
                dir.mkdirs();
            }
            jsonObject.put("result", file.createNewFile());
        }
        return jsonObject.toString();
    }

    @Tool(description = "create multi empty file", schema = multiFileSchema)
    public List<String> createMultiEmptyFile(Map<String, Object> params) {
        JSONArray paths = JSONArray.parseArray(JSONArray.toJSONString(params.get("paths")));
        return paths.stream().map(x -> {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("path", x);
            try {
                File file = new File(x.toString());
                if (file.exists()) {
                    jsonObject.put("result", "file already exist");
                } else {
                    File dir = file.getAbsoluteFile().getParentFile();
                    if (!dir.exists()) {
                        dir.mkdirs();
                    }
                    jsonObject.put("result", file.createNewFile());
                }
            } catch (IOException e) {
                jsonObject.put("result", JSONObject.toJSONString(e));
                jsonObject.put("isError", true);
            }
            return jsonObject.toString();
        }).toList();
    }


    @Tool(description = "modify text file's content",
            schema = fileModifySchema)
    public String modifyLinesTextFile(Map<String, Object> params) throws IOException {
        JSONObject jsonObject = new JSONObject();
        String path = params.get("path").toString();
        jsonObject.put("path", path);
        String lines = JSONArray.toJSONString(params.get("lines"));
        JSONArray objects = JSONArray.parseArray(lines);
        Path filePath = Path.of(path);
        List<String> strings = Files.readAllLines(filePath, StandardCharsets.UTF_8);
        objects.forEach(x -> {
            JSONObject data = JSONObject.parseObject(JSONObject.toJSONString(x));
            int lineIndex = data.getIntValue("line_num") - 1;
            boolean insert = data.getBooleanValue("insert");
            if (lineIndex < strings.size() && !insert) {
                strings.set(lineIndex, data.getString("newContent"));
            } else {
                strings.add(lineIndex, data.getString("newContent"));
            }
        });
        Files.write(filePath, strings, StandardCharsets.UTF_8);
        jsonObject.put("result", "modify success");
        jsonObject.put("newContent", getFileAllLines(filePath));

        return jsonObject.toString();
    }

    @Tool(description = "delete text file lines",
            schema = fileDeleteLinesSchema)
    public String deleteLinesTextFile(Map<String, Object> params) throws IOException {
        JSONObject jsonObject = new JSONObject();
        String path = params.get("path").toString();
        jsonObject.put("path", path);
        String lines = JSONArray.toJSONString(params.get("lines"));
        JSONArray objects = JSONArray.parseArray(lines);
        Path filePath = Path.of(path);
        List<String> strings = Files.readAllLines(filePath, StandardCharsets.UTF_8);
        objects.stream().forEach(x -> {
            JSONObject data = JSONObject.parseObject(JSONObject.toJSONString(x));
            strings.set(data.getIntValue("line_num") - 1, null);
        });
        Files.write(filePath, strings.stream().filter(Objects::nonNull).toList(), StandardCharsets.UTF_8);
        jsonObject.put("result", "delete lines success");
        jsonObject.put("newContent", getFileAllLines(filePath));
        return jsonObject.toString();
    }


    @Tool(description = "write file with data", schema = fileWriteSchema)
    public String writeFile(Map<String, Object> params) throws IOException {
        JSONObject jsonObject = new JSONObject();
        String path = params.get("path").toString();
        jsonObject.put("path", path);
        jsonObject.put("isBytes", params.get("isBytes"));
        String data = params.get("data").toString();
        File dir = new File(path).getAbsoluteFile().getParentFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }
        File file = new File(path);
        if (file.exists() && file.isDirectory()) {
            jsonObject.put("result", "not a file , it is a directory");
            jsonObject.put("isError", true);
        } else {
            boolean append = Boolean.parseBoolean(params.getOrDefault("append", false).toString());
            Path dataPath;
            Path source = Paths.get(path);
            byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
            if (jsonObject.getBooleanValue("isBytes")) {
                bytes = Base64.getDecoder().decode(data);
            }
            if (append) {
                dataPath = Files.write(source, bytes,
                        StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
            } else {
                dataPath = Files.write(source, bytes,
                        StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
            }
            File resfile = dataPath.toFile();
            if (resfile.exists() && resfile.length() > 0) {
                jsonObject.put("result", true);
                jsonObject.put("newContent", "write success");
            } else {
                jsonObject.put("result", "write failed");
                jsonObject.put("isError", true);
            }
        }
        return jsonObject.toString();
    }


    @Tool(description = "write multi file with data", schema = writeMultiFileSchema)
    public List<String> writeMultiTextFile(Map<String, Object> params) {
        JSONArray paths = JSONArray.parseArray(JSONArray.toJSONString(params.get("paths")));
        return paths.stream().map(x -> {
            JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(x));
            String path = jsonObject.getString("path");
            boolean isBytes = jsonObject.getBooleanValue("isBytes");
            File dir = new File(path).getAbsoluteFile().getParentFile();
            if (!dir.exists()) {
                dir.mkdirs();
            }
            JSONObject res = new JSONObject();
            if (new File(path).isDirectory()) {
                res.put("result", "not a file,it is a directory");
                res.put("isError", true);
            } else {
                res.put("path", path);
                String data = jsonObject.getString("data");
                try {
                    Path dataPath;
                    Path source = Path.of(path);
                    boolean append = Boolean.parseBoolean(params.getOrDefault("append", false).toString());
                    byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
                    if (isBytes) {
                        bytes = Base64.getDecoder().decode(data);
                    }
                    if (append) {
                        dataPath = Files.write(source, bytes,
                                StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
                    } else {
                        dataPath = Files.write(source, bytes,
                                StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
                    }
                    res.put("result", dataPath.toFile().length() > 0);
                    res.put("newContent", "write success");
                } catch (IOException e) {
                    res.put("result", JSONObject.toJSONString(e));
                    res.put("isError", true);
                }
            }
            return res.toString();
        }).toList();
    }


    @Tool(description = "list directory", schema = fileSchema)
    public String listDirectory(Map<String, Object> params) {
        String path = params.get("path").toString();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("path", path);
        return listDir(path, jsonObject);
    }

    @Tool(description = "find a file in directory path", schema = findFileSchema)
    public String findFile(Map<String, Object> params) {
        String path = params.get("path").toString();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("path", path);
        jsonObject.put("file", params.get("file"));
        return findFile(jsonObject);
    }

    private String findFile(JSONObject jsonObject) {
        try (Stream<Path> paths = Files.walk(Path.of(jsonObject.getString("path")))) {
            List<String> file = paths // 过滤出文件
                    .filter(path -> path.getFileName().toString().equals(jsonObject.getString("file"))) // 过滤出目标文件名
                    .map(path -> path.toAbsolutePath().toString()).toList();// 打印找到的文件路径
            jsonObject.put("result", file);
        } catch (IOException e) {
            jsonObject.put("result", JSONObject.toJSONString(e));
            jsonObject.put("isError", true);
        }

        return jsonObject.toString();
    }

    private String listDir(String path, JSONObject jsonObject) {
        File[] files = new File(path).listFiles();
        if (Objects.nonNull(files) && files.length > 0) {
            jsonObject.put("result", JSONObject
                    .toJSONString(Arrays.stream(files).map(x -> {
                        JSONObject jsonObject1 = new JSONObject();
                        jsonObject1.put("path", x.getName());
                        jsonObject1.put("type", x.isFile() ? "file" : "directory");
                        return jsonObject1;
                    }).toList()));
        } else {
            jsonObject.put("result", "[]");
        }

        return jsonObject.toString();
    }

    @Tool(description = "list multi directory", schema = multiFileSchema)
    public List<String> listMultiDirectory(Map<String, Object> params) {
        JSONArray paths = JSONArray.parseArray(JSONArray.toJSONString(params.get("paths")));
        return paths.stream().map(y -> {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("path", y);
            return listDir(y.toString(), jsonObject);
        }).toList();
    }

    @Tool(description = "create  directory", schema = fileSchema)
    public String createDirectory(Map<String, Object> params) {
        String path = params.get("path").toString();
        JSONObject jsonObject = new JSONObject();
        File file = new File(path);
        if (file.exists()) {
            jsonObject.put("path", path);
            jsonObject.put("result", "dir already exist");
            jsonObject.put("isError", false);
        } else {
            jsonObject.put("path", path);
            jsonObject.put("result", file.mkdirs());
        }
        return jsonObject.toString();
    }

    @Tool(description = "create multi directory", schema = multiFileSchema)
    public List<String> createMultiDirectory(Map<String, Object> params) {
        JSONArray paths = JSONArray.parseArray(JSONArray.toJSONString(params.get("paths")));
        return paths.stream().map(x -> {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("path", x);
            File file = new File(x.toString());
            if (file.exists()) {
                jsonObject.put("isError", false);
                jsonObject.put("result", "dir has exists");
            } else {
                boolean mkdirs = file.mkdirs();
                if (mkdirs) {
                    jsonObject.put("isError", false);
                    jsonObject.put("result", "create success");
                } else {
                    jsonObject.put("isError", true);
                    jsonObject.put("result", "create failed");
                }
            }
            return jsonObject.toString();
        }).toList();
    }

    @Tool(description = "delete directory", schema = fileSchema)
    public String deleteDirectory(Map<String, Object> params) throws IOException {
        String path = params.get("path").toString();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("path", path);
        deleteDir(path, jsonObject);
        return jsonObject.toString();
    }

    @Tool(description = "delete multi directory", schema = multiFileSchema)
    public List<String> deleteMultiDirectory(Map<String, Object> params) {
        JSONArray paths = JSONArray.parseArray(JSONArray.toJSONString(params.get("paths")));
        return paths.stream().map(x -> {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("path", x);
            try {
                deleteDir(x.toString(), jsonObject);
            } catch (IOException e) {
                jsonObject.put("result", JSONObject.toJSONString(e));
                jsonObject.put("isError", true);
            }

            return jsonObject.toString();
        }).toList();
    }


    @Tool(description = "check file or directory stat", schema = fileSchema)
    public String checkFileStat(Map<String, Object> params) {
        String path = params.get("path").toString();
        JSONObject jsonObject = new JSONObject();
        File file = new File(path);
        jsonObject.put("path", path);
        jsonObject.put("result", new FileInfo(file.isFile() ? "file" : "directory", file.length(),
                file.canRead(), file.canWrite(), file.canExecute(),
                file.isAbsolute(), file.lastModified(), file.isHidden(), file.getTotalSpace(), file.getFreeSpace(), file.getUsableSpace(), file.exists()));
        return jsonObject.toString();
    }

    @Tool(description = "check multi file or directory stat", schema = multiFileSchema)
    public List<String> checkMultiFileStat(Map<String, Object> params) {
        JSONArray paths = JSONArray.parseArray(JSONArray.toJSONString(params.get("paths")));
        return paths.stream().map(x -> {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("path", x);
            File file = new File(x.toString());
            jsonObject.put("result", new FileInfo(file.isFile() ? "file" : "directory", file.length(),
                    file.canRead(), file.canWrite(), file.canExecute(),
                    file.isAbsolute(), file.lastModified(), file.isHidden(), file.getTotalSpace(), file.getFreeSpace(), file.getUsableSpace(), file.exists()));
            return jsonObject.toString();
        }).toList();
    }

    private void deleteDir(String x, JSONObject jsonObject) throws IOException {
        Path path = Files.walkFileTree(Paths.get(x), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (file.toFile().exists()) {
                    Files.delete(file);
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                if (dir.toFile().exists()) {
                    Files.delete(dir);
                }
                return FileVisitResult.CONTINUE;
            }
        });
        jsonObject.put("result", !path.toFile().exists());
    }


    public static void main(String[] args) throws Exception {
        McpServerManager mcpServerManager = new McpServerManager();
        mcpServerManager.initStdioServer("mcp-filesystem", "1.0.0", FileSystemMCPServer.class,
                McpSchema.ServerCapabilities.builder()
                        .tools(true)
                        .logging()
                        .build());
    }


    public record FileInfo(String type, long size, boolean canRead, boolean canWrite, boolean canExecute,
                           boolean isAbsolute, long lastModified, boolean isHidden, long totalSpace, long freeSpace,
                           long usableSpace, boolean exists) {
    }

    public record FileLine(int line, String content) {
        @Override
        public String toString() {
            return new StringJoiner(":")
                    .add(line+"")
                    .add(content)
                    .toString();
        }
    }

}
